/*
 *  Copyright (C) 2011  Clint Priest
 *
 *  This file is part of Snap Graph.
 *
 *  Snap Graph is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Snap Graph is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with Snap Graph.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

/* Loads the script into the SnapLinksContext object */
LoadScriptInBrowserContext('chrome://SnapGraph/content/lib/raphael.js');
LoadScript('chrome://SnapGraph/content/Browser/GraphPlot.js');

var Graph = Class.create(Raphael, {
	Margin: 	10,
	
	ColorSchemes: [
		[ '#FF0000', '#00FF00', '#FFFF00', '#FF00FF', ]
	],
	
	get DataSelections() {	return this._DataSelections;	},
	set DataSelections(x) {
		this._DataSelections = x;
		this.Calculate();
	},
	
	initialize: function(width, height) {
		this.width = width;
		this.height = height;
		
		this.VerticalAxes = [ ];
		this.Paper = Raphael(0, 0, width, height);
		this.SetSize(width, height);
	},
	
	Calculate: function() {
		/* For some reason (tm) without the timeout here, font bounding box calculations cannot be made */
		setTimeout( function() {
			try {
				this.InitializeAxes();
				this.InitializeDataPoints();
			} catch(e) { 
				console.log('%s: %s(%s) %o', e.message, e.fileName, e.lineNumber, e.stack);
			}
		}.bind(this), 0);
	},
	/* Determines how many Axes should exist and initializes them */
	InitializeAxes: function() {
		this.VerticalAxes = [ ];
		
		var AxesSets = [ ];
		var HorizontalValues = { };
		/* Determine which data selections belong to which axes */
		this.DataSelections.forEach( function( ds ) {
			Object.keys(ds.Data).forEach( function(val) {
				HorizontalValues[val] = 1;
			} );
			var vs = new ValueSet(Object.values(ds.Data));
			var AxisNumber = AxesSets.reduce( function( acc, axis, AxisNumber ) {
				if(acc < 0 && vs.Max > (axis.Min / 2))
					return AxisNumber;
				return acc;
			}, -1);
			if(AxisNumber < 0) {
				AxesSets.push( new ValueSet(vs) );
				AxisNumber = 0;
			} else {
				AxesSets[AxisNumber] = AxesSets[AxisNumber].concat(vs);
			}
			ds.VerticalAxisGroup = AxisNumber;
		});
		this.HorizontalAxis = new HorizontalAxis(this, 10, new ValueSet(Object.keys(HorizontalValues)));

		AxesSets.forEach( function(AxisSet) {
			var axis = new VerticalAxis(this, 6, AxisSet );
			axis.SetPosition(new Rect(this.GraphRect.top, this.GraphRect.left, this.GraphRect.bottom - this.HorizontalAxis.Height - 5, this.GraphRect.left + axis.Width) );
			this.VerticalAxes.push( axis );
			axis.show();
		}, this);
		
		this.HorizontalAxis.SetPosition(new Rect(this.GraphRect.bottom - this.HorizontalAxis.Height, this.GraphRect.left + this.VerticalAxes[0].Width, this.GraphRect.bottom, this.GraphRect.right));
		this.HorizontalAxis.show();
		
		var VABox = this.VerticalAxes[0].AxisBox,
			HABox = this.HorizontalAxis.AxisBox;
		
		this.PlotRect = new Rect(VABox.y, HABox.x, VABox.y + VABox.height, HABox.x + HABox.width);
				
//		AxesSets[0].Sets[0].Data.forEach( function(val) {
//			console.log( [ this.Axes[0].LowRange, this.Axes[0].HighRange, val, this.Axes[0].TranslateValue(val)].join(', '));
//		}, this);
	},
	InitializeDataPoints: function() {
		var ColorSet = 0;
		var ColorAssignment = 0;
		
		this.DataSelections.forEach( function(ds) {
			ds.Coordinates = Object.keys(ds.Data).reduce( function(acc, key) {
				acc.push ( {
					x: this.HorizontalAxis.TranslateValue(key),
					y: this.VerticalAxes[ds.VerticalAxisGroup].TranslateValue(ds.Data[key]),
				} );
				return acc;
			}.bind(this), [ ]);
			var ps = new PlotScheme(this.ColorSchemes[ColorSet][ColorAssignment]);
			if(ColorAssignment == 0)
				ds.DataPlot = new AverageBars(this, ds, ps);
			else
				ds.DataPlot = new AreaPlot(this, ds, ps);
			ColorAssignment++;
		}, this );
		this.DataSelections.forEach( function(ds) {
			ds.DataPlot.AdjustLayers();
		});
	},
	SetSize: function(width, height) {
		this.Paper.clear();
		this.Paper.setSize(width, height);
		this.Background = this.Paper.rect(0, 0, width, height).attr( { fill: '#000000' } );
		this.GraphRect = new Rect(0, 0, height, width);
		this.GraphRect.Shrink(this.Margin, this.Margin);
	
		if(this._DataSelections) {
			clearTimeout(this.CalcTimer);	
			this.CalcTimer = setTimeout( function() {
				this.Calculate();
			}.bind(this), 100);
		}
	}
} );

var Axis = Class.create( {
	initialize: function(gr, ticks, values) {
		this.Graph = gr;
		this.Values = values;
		this.Ticks = ticks;
		
		this.CreateLabels();
	},

	get HighValue() {
		return this.Values.Max;
	},
	get LowValue() 	{
		return this.Values.Min; 
	},
	
	get HighRange() {
		var high = this.HighValue;
		if(high < 10 && high > -10)
			return Math.ceil(high);
		var div = parseInt((new String(Math.floor(high/10))).replace(/\d/g, 0).replace(/\d/, 1));
		if(high < 0)	/* Treat Negative Numbers Differently by going closer to zero */
			return (Math.ceil(high / div)*div) - div;
		return Math.ceil(high / div)*div;
	},
	get LowRange() {
		var low = Math.floor(this.LowValue);
		if(low < 10 && low > -10)
			return Math.floor(low);
		var div = parseInt((new String(Math.floor(low/10))).replace(/\d/g, 0).replace(/\d/, 1));
		if(low < 0)		/* Treat Negative Numbers Differently by going further from zero */
			return (Math.floor(low / div)*div) + div;
		return Math.floor(low / div)*div;
	},
	get MaxLabelDimensions() {
		var HighDims = this.Labels.reduce( function(HighDims, Label ) {
			var dims = Label.getBBox();
			
			HighDims.x = Math.max(HighDims.x, dims.width);
			HighDims.y = Math.max(HighDims.y, dims.height);

			return HighDims;
		}, { x: -Number.MAX_VALUE, y: -Number.MAX_VALUE } );
		
		return { x: Math.ceil(HighDims.x), y: Math.ceil(HighDims.y) };
	},
	
	get AxisBox() { return this.Axis.getBBox(); },
	
	show: function() {
		this.Axis.animate( { opacity: 1 }, 1000 );

		var delay = 0;
		
		this.Labels.forEach( function( label ) {
			setTimeout( function() {
				label.animate( { opacity: 1 }, 500 );
			}, delay);
			delay += 75;
		} );
	},
	
	TranslateValue: function(val) {
		var LineBox = this.Axis.getBBox();
		
		var Low = this.LowRange,
			High = this.HighRange;
			
		if(this.Values.Formatter.Type == 'Date') {
			Low = this.LowValue;
			High= this.HighValue;
		}
		var Range = High - Low;
				
		var out = (this.Values.Formatter.NumericValue(val) - Low) / Range;
		if(LineBox.width > LineBox.height)
			return (out * LineBox.width) + LineBox.x;
		return (LineBox.height - (out * LineBox.height)) + LineBox.y;
	},

	CreateLabels: function() {
		(this.Labels || [ ]).forEach( function (Label) {
			Label.remove();
		} );
		this.Labels = [ ];

		var LowRange = this.LowRange,
			HighRange = this.HighRange,
			Range = HighRange - LowRange;
			
		if(this.Values.Formatter.Type == 'Date') {
			LowRange = this.LowValue;
			HighRange = this.HighValue;
			Range = HighRange - LowRange;
		}
		for(var j=0; j < this.Ticks; j++) {
			var text = this.FormatLabel(LowRange + (Range * (j/(this.Ticks-1))));			
			
			this.Labels.push(
				this.Graph.Paper.text(0, -1000, text)
					.attr( { opacity: 0, fill: 'white', 'text-anchor': this.LabelTextAnchor } )
			);
		}
	},
	FormatLabel: function(text) {
		return this.Values.Formatter.Format(text);
	}
});

var HorizontalAxis = Class.create( Axis, {
	LabelTextAnchor: 'middle',
	
	initialize: function($super, gr, ticks, values) {
		$super(gr, ticks, values);
	},
	
	SetPosition: function(rect) {
		if(this.Axis)
			this.Axis.remove();
		
		this.Axis = this.Graph.Paper.path( [
				'M' + (rect.left + (this.Labels[0].getBBox().width/2) ) + ',' + (rect.bottom - this.MaxLabelDimensions.y),
				'L' + (rect.right - (this.Labels.last().getBBox().width/2) ) + ',' + (rect.bottom - this.MaxLabelDimensions.y),
			].join(' ')
		).attr( { opacity: 0, fill: 'none', stroke: '#FFFFFF', 'stroke-width': 1 } );
		this.Axis.node.setAttribute('shape-rendering', 'crispEdges');
		
		this.PositionLabels();
	},

	get Height() { return this.MaxLabelDimensions.y; },
	
	PositionLabels: function() {
		var LineBox = this.Axis.getBBox();
		var LabelCount = this.Labels.length;
		
		this.Labels.forEach( function(Label, index) {
			var Box = Label.getBBox();
			
			Label.node.setAttribute('x', LineBox.x + ((index / (LabelCount-1)) * LineBox.width) );
			Label.node.setAttribute('y', LineBox.y + Box.height );
		} );
	},
} );

var VerticalAxis = Class.create( Axis,  {
	LabelTextAnchor:	'end',
	
	initialize: function($super, gr, ticks, values) {
		$super(gr, ticks, values);
	},
	SetPosition: function(rect) {
		if(this.Axis)
			this.Axis.remove();
		
		this.Axis = this.Graph.Paper.path( [
				'M' + (rect.left + this.MaxLabelDimensions.x) + ',' + rect.top,
				'L' + (rect.left + this.MaxLabelDimensions.x) + ',' + rect.bottom
			].join(' ')
		).attr( { opacity: 0, fill: 'none', stroke: '#FFFFFF', 'stroke-width': 1 } );
		this.Axis.node.setAttribute('shape-rendering', 'crispEdges');

		this.PositionLabels();
	},
		
	get Width() { return this.MaxLabelDimensions.x; },
	
	PositionLabels: function() {
		var LineBox = this.Axis.getBBox();
		var LabelCount = this.Labels.length;
		
		this.Labels.concat().reverse().forEach( function(Label, index) {
			var Box = Label.getBBox(),
				yOffset = -5;
				
			if(index == LabelCount-1)
				yOffset = -Box.height-1;
			Label.node.setAttribute('x', LineBox.x - 3);
			Label.node.setAttribute('y', LineBox.y + Box.height + yOffset + ((index / (LabelCount-1)) * LineBox.height));
		} );
	},
} );

